import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Scanner;

import org.apache.commons.codec.binary.Base64;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class QueryOptimizer {
	private class QueryResponse {
		boolean isRelevant;

		String title;
		String description;
		String url;
	}

	private ArrayList<String> keywords;

	private double targetPrecision = 0;
	private double currentPrecision = 0;
	private int countRelevant = 0;
	private URL url;

	private ArrayList<QueryResponse> responses;

	private static Scanner scanner = new Scanner(System.in);

	/**
	 * Number of responses to consider for each query.
	 */
	private static final int nResponses = 10;

	/**
	 * Static method to create an instance of QueryOptimizer
	 * 
	 * @return
	 */
	public static QueryOptimizer createQueryOptimizer() {
		System.out.println("Please enter your query.");

		String[] keywords = scanner.nextLine().split("\\s+");

		System.out.println("Please enter your precision.");
		double targetPrecision = scanner.nextDouble();
		scanner.nextLine();

		QueryOptimizer qo = new QueryOptimizer(keywords, targetPrecision);
		return qo;
	}

	/**
	 * Private constructor to disallow invocation outside the class.
	 * 
	 * @param keywords
	 * @param targetPrecision
	 */
	private QueryOptimizer(String[] keywords, double targetPrecision) {
		if (keywords == null || keywords.length == 0)
			throw new IllegalArgumentException(
					"Keywords must not be null or empty.");

		if (targetPrecision < 0.0 || targetPrecision > 1.0)
			throw new IllegalArgumentException(
					"targetPrecision must be between 0 and 1");

		this.keywords = new ArrayList<String>(Arrays.asList(keywords));
		this.targetPrecision = targetPrecision;
		this.responses = new ArrayList<QueryResponse>();
		this.url = null;
	}

	/**
	 * Call Bing's API to execute query based on keywords. Store results in
	 * responses.
	 * 
	 * @throws JSONException
	 */
	public void executeQuery() throws IOException, JSONException {
		String bingUrlBef = "https://api.datamarket.azure.com/Data.ashx/Bing/SearchWeb/v1/Web?Query=%27";
		String bingUrlMid = keywords.get(0);
		for (int i = 1; i < keywords.size(); i++)
			bingUrlMid = bingUrlMid + "%20" + keywords.get(i);
		// System.out.println(bingUrlMid);
		String bingUrlAft = "%27&$top=10&$format=Json";

		// Provide your account key here.
		String accountKey = "g8MIzS0dlBDoWFt1069DkhomL/LhZrv/hZGqu/E03aA=";

		byte[] accountKeyBytes = Base64
				.encodeBase64((accountKey + ":" + accountKey).getBytes());
		String accountKeyEnc = new String(accountKeyBytes);

		url = new URL(bingUrlBef + bingUrlMid + bingUrlAft);
		URLConnection urlConnection = url.openConnection();
		urlConnection.setRequestProperty("Authorization", "Basic "
				+ accountKeyEnc);

		InputStream inputStream = (InputStream) urlConnection.getContent();
		byte[] contentRaw = new byte[urlConnection.getContentLength()];
		inputStream.read(contentRaw);
		String content = new String(contentRaw);

		// The content string is the xml/json output from Bing.
		// System.out.println(content);

		JSONObject myjson = new JSONObject(content);
		JSONArray the_json_array = myjson.getJSONObject("d").getJSONArray(
				"results");

		responses.clear();
		int size = the_json_array.length();
		for (int i = 0; i < size; i++) {
			JSONObject another_json_object = the_json_array.getJSONObject(i);
			QueryResponse one = new QueryResponse();
			another_json_object.getString("ID");
			one.title = another_json_object.getString("Title");
			one.description = another_json_object.getString("Description");
			another_json_object.getString("DisplayUrl");
			one.url = another_json_object.getString("Url");

			responses.add(one);
		}
	}

	public double getCurrentPrecision() {
		currentPrecision = (double) countRelevant / nResponses;
		return currentPrecision;
	}

	public double getTargetPrecision() {
		return targetPrecision;
	}

	/**
	 * Perform the query expansion algorithm. Improve precision by adding
	 * keywords based on user's relevance mark.
	 */
	public void improveQuery() {
		final double a = 1, b = 0.8, c = 0.1;
		int totalResponses = responses.size();

		HashMap<String, Integer> allWordCount = new HashMap<String, Integer>();
		ArrayList<HashMap<String, Integer>> docWordCounts = new ArrayList<HashMap<String, Integer>>(
				totalResponses);

		// Count words
		for (QueryResponse response : responses) {
			HashMap<String, Integer> curDocWordCount = new HashMap<String, Integer>();

			String[] titleWords = response.title.split("[^a-zA-Z-]+");
			
			ArrayList<String> words = new ArrayList<String>(Arrays.asList(titleWords));
//			words.addAll(Arrays.asList(titleWords));
//			words.addAll(Arrays.asList(titleWords));

			for (String word : words) {
				word = word.toLowerCase();

				if (!curDocWordCount.containsKey(word))
					incrementValue(allWordCount, word);

				incrementValue(curDocWordCount, word);
			}

			docWordCounts.add(curDocWordCount);
		}

		// Calculate weights
		ArrayList<HashMap<String, Double>> weightVecs = new ArrayList<HashMap<String, Double>>(
				totalResponses);
		for (int i = 0; i < totalResponses; i++) {
			HashMap<String, Double> curWeightVec = new HashMap<String, Double>();
			HashMap<String, Integer> curWordCount = docWordCounts.get(i);

			for (String word : curWordCount.keySet()) {
				double weight = curWordCount.get(word)
						* Math.log((double) totalResponses
								/ allWordCount.get(word));
				curWeightVec.put(word, weight);
			}

			weightVecs.add(curWeightVec);
		}

		// Calculate current query weights
		HashMap<String, Double> queryWeight = new HashMap<String, Double>();
		for (String word : keywords) {
			double weight = Math.log((double) totalResponses
					/ allWordCount.get(word));
			queryWeight.put(word, weight);
		}

		// Calculate sum of weight vectors
		HashMap<String, Double> relevantVec = new HashMap<String, Double>();
		HashMap<String, Double> nonRelevantVec = new HashMap<String, Double>();

		for (int i = 0; i < totalResponses; i++) {
			if (responses.get(i).isRelevant)
				addWeightVec(relevantVec, weightVecs.get(i));
			else
				addWeightVec(nonRelevantVec, weightVecs.get(i));
		}

		// Normalize
		for (String key : relevantVec.keySet())
			relevantVec.put(key, relevantVec.get(key) / countRelevant);
		for (String key : nonRelevantVec.keySet())
			nonRelevantVec.put(key, nonRelevantVec.get(key)
					/ (totalResponses - countRelevant));

		// Sum it up
		HashMap<String, Double> finalMap = new HashMap<String, Double>();

		for (String key : queryWeight.keySet())
			finalMap.put(key, queryWeight.get(key) * a);

		for (String key : relevantVec.keySet()) {
			if (finalMap.containsKey(key))
				finalMap.put(key, finalMap.get(key) + b * relevantVec.get(key));
			else
				finalMap.put(key, b * relevantVec.get(key));
		}

		for (String key : nonRelevantVec.keySet()) {
			if (finalMap.containsKey(key))
				finalMap.put(key,
						finalMap.get(key) - c * nonRelevantVec.get(key));
			else
				finalMap.put(key, -c * nonRelevantVec.get(key));
		}
		
		// Find top
		double max = Double.MIN_VALUE;
		String topKeyWord = null;
		for (Entry<String, Double> entry : finalMap.entrySet()) {
			if (!keywords.contains(entry.getKey()) && entry.getValue() > max) {
				max = entry.getValue();
				topKeyWord = entry.getKey();
			}
		}

		System.out.println("Keyword added: " + topKeyWord);

		keywords.add(topKeyWord);
	}

	private void addWeightVec(HashMap<String, Double> sum,
			HashMap<String, Double> vec) {
		for (String word : vec.keySet()) {
			if (sum.containsKey(word))
				sum.put(word, sum.get(word) + vec.get(word));
			else
				sum.put(word, vec.get(word));
		}
	}

	private void incrementValue(HashMap<String, Integer> curDocWordCount,
			String word) {
		if (curDocWordCount.containsKey(word))
			curDocWordCount.put(word, curDocWordCount.get(word) + 1);
		else
			curDocWordCount.put(word, 1);
	}

	/**
	 * Display responses from previous query, let user judge which one is
	 * relevant. Update mark of the corresponding response. Update current
	 * precision.
	 */
	public void judgeResponse() {
		// reset
		currentPrecision = 0;
		countRelevant = 0;

		printQueryInfo();
		System.out.println("Bing Search Results:");
		System.out.println("====================");

		for (int i = 0; i < responses.size(); i++) {
			System.out.println("Result " + (i + 1));

			System.out.println("[");
			System.out.println("URL:" + responses.get(i).url);
			System.out.println("Title:" + responses.get(i).title);
			System.out.println("Summary:" + responses.get(i).description);
			System.out.println("]");
			System.out.print("Relevant (Y/N)?");

			String yOrn = scanner.nextLine();

			if (yOrn.toLowerCase().equals("y")) {
				responses.get(i).isRelevant = true;
				countRelevant++;
			} else
				responses.get(i).isRelevant = false;

		}
	}

	private void printQueryInfo() {
		System.out.println("Parameters:");
		System.out
				.println("Client key = g8MIzS0dlBDoWFt1069DkhomL/LhZrv/hZGqu/E03aA=");

		String query = "";
		for (String s : keywords) {
			query = query + " " + s;
		}
		System.out.println("Query =" + query);

		System.out.println("Precision = " + targetPrecision);
		System.out.println("URL: " + url);
		System.out.println("Total no of results : " + responses.size());
	}
}
